package com.google.code.lf.commons.aspect.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Service;

import com.google.code.lf.commons.aspect.DontLogIt;
import com.google.code.lf.commons.aspect.Loggable;
import com.google.code.lf.commons.aspect.ProtectArgsFromLogging;
import com.google.code.lf.commons.exception.BusinessException;
import com.google.code.lf.commons.exception.ManagerException;

/**
 * Cette classe implémente un aspect de gestion de logs et d'exceptions.
 * <br/>Cette classe est notamment utilisée par les managers et services de l'application.
 * <br/>Les méthodes publiques d'une classe annotée avec {@link Loggable} sont interceptées, hors constructeurs, getters, setters, méthodes to* et méthodes annotées avec {@link DontLogIt}.
 * <br/>Il est également possible de masquer certains arguments en utilisant l'annotation {@link ProtectArgsFromLogging}.
 */
@Aspect
@Service(LoggingAspect.BEAN_ID)
public class LoggingAspect {
	/** Id de bean Spring : loggingAspect. */
	public static final String BEAN_ID = "loggingAspect";

	/** Logger. */
	private static final Logger logger = Logger.getLogger(LoggingAspect.class);

	/**
	 * Map permettant de stocker les temps d'exécution.
	 * <br/>L'aspect est un singleton + les manager et service s'appellent entre eux + chaque appel de méthode est intercepté
	 * => on ne peut se satisfaire de simples variables membres : le temps d'exécution doit correspondre à une interception, et donc à un point de coupe.
	 */
	private static final HashMap<JoinPoint, Long> execTime = new HashMap<JoinPoint, Long>();

	/**
	 * Définit le point de coupe de niveau classe.
	 * <br/>Les classes annotées <code>@</code>{@link Loggable} sont interceptées.
	 */
	@Pointcut("within(@com.google.code.lf.commons.aspect.Loggable *)")
	public void myClass() {
	}

	/**
	 * Définit le point de coupe de niveau méthode.
	 * <br/>Les méthodes interceptées sont :<ul>
	 * <li>Les méthodes publiques des classes interceptées (cf. {@link #myClass()}.
	 * <li>et qui ne sont pas annotées <code>@</code>{@link DontLogIt}.
	 * <li>et qui ne sont pas de type "to*" (toString, toXML, etc).</ul>
	 */
	@Pointcut("com.google.code.lf.commons.aspect.impl.LoggingAspect.myClass() && execution(public * *.*(..)) && !execution(* get*(..)) && !execution(* set*(..)) && !execution(* to*(..)) && !execution(@com.google.code.lf.commons.aspect.DontLogIt * *.*(..))")
	public void myMethod() {
	}

	// NB : cette méthode est sans effet : le logger reste celui de l'aspect
	/*private void setLogger(Class<?> clazz) {
		logger = Logger.getLogger(clazz);
	}*/

	/**
	 * Implémente le comportement avant interception d'une méthode.
	 *
	 * @param joinPoint Le point de coupe ({@link JoinPoint}), voir {@link #myMethod()}.
	 */
	@Before("com.google.code.lf.commons.aspect.impl.LoggingAspect.myMethod()")
	public void logEntering(final JoinPoint joinPoint) {
		if (logger.isDebugEnabled()) {
			// Modification du logger courant
			//setLogger(thisJoinPoint.getSignature().getDeclaringType());

			// Gestion de la durée d'exécution
			final long startTime = System.currentTimeMillis();
			execTime.put(joinPoint, startTime);

			// Gestion du log
			doLog("->", getMessage(joinPoint), 0L);
		}
	}

	/**
	 * Implémente le comportement après interception d'une méthode.
	 *
	 * @param joinPoint Le point de coupe ({@link JoinPoint}), voir {@link #myMethod()}.
	 */
	@After("com.google.code.lf.commons.aspect.impl.LoggingAspect.myMethod()")
	public void logExiting(final JoinPoint joinPoint) {
		if (logger.isDebugEnabled()) {
			// Gestion de la durée d'exécution
			final Long start = execTime.get(joinPoint);
			long elapsedTime = 0;
			if (start != null) {
				final long startTime = start;
				final long endTime = System.currentTimeMillis();
				elapsedTime = endTime - startTime;
			}
			execTime.remove(joinPoint);

			// Gestion du log
			doLog("<-", getMessage(joinPoint), elapsedTime);
		}
	}

	/**
	 * Implémente le comportement en cas d'exception sur une méthode interceptée.
	 * <br/>Les {@link BusinessException} ne sont pas tracées.
	 * <br/>Les {@link ManagerException} sont tracées remontées telles quelles.
	 * <br/>Par défaut, les exceptions sont traitées et remontées en tant que {@link ManagerException}.
	 *
	 * @param joinPoint Le point de coupe ({@link JoinPoint}), voir {@link #myMethod()}.
	 * @param exception L'{@link Exception} lancée par la méthode interceptée.
	 */
	@AfterThrowing(pointcut = "com.google.code.lf.commons.aspect.impl.LoggingAspect.myMethod()", throwing = "exception")
	public void logException(final JoinPoint joinPoint, final Exception exception) {
		// On ne trace pas les BusinessException
		if (exception instanceof BusinessException) {
			throw (BusinessException) exception;
		}

		// Log de l'exception
		logger.error(getMessage(joinPoint), exception);

		// Cas d'une ManagerException
		if (exception instanceof ManagerException) {
			throw (ManagerException) exception;
		}

		// Cas par défaut : on lève toujours une ManagerException
		throw new ManagerException(exception.getMessage(), exception);
	}

	/**
	 * Génère une trace de log.
	 *
	 * @param prefix Le préfixe de la chaîne de caractères à tracer. Peut être <code>null</code>.
	 * @param msg La chaîne de caractères à tracer. Peut être <code>null</code>.
	 * @param time La durée d'exécution. Tracée si > 0 uniquement.
	 */
	private void doLog(final String prefix, final String msg, final long time) {
		if (logger.isDebugEnabled()) {
			final StringBuilder log = new StringBuilder();

			// Préfixe
			if (prefix != null) log.append(prefix).append(" ");
			// Message
			if (msg != null) log.append(msg);
			// Durée d'exécution
			if (time > 0) log.append(" : ").append(time).append(" ms");

			if (log.length() > 0) log.append(".");
			logger.debug(log.toString());
		}
	}

	/**
	 * Génère un message de log pour un point de coupe.
	 *
	 * @param joinPoint Le point de coupe ({@link JoinPoint}).
	 * @return La chaîne de caractères à tracer.
	 *
	 * @see #getArgs(JoinPoint).
	 */
	private String getMessage(final JoinPoint joinPoint) {
		final StringBuilder msg = new StringBuilder();

		// Nom de la classe interceptée
		msg.append(joinPoint.getSignature().getDeclaringTypeName());
		// Nom de la méthode interceptée
		msg.append(".").append(joinPoint.getSignature().getName());
		// Détail sur les arguments d'appel
		msg.append(getArgs(joinPoint));

		return msg.toString();
	}

	/**
	 * Fournit des détails sur les arguments d'appel pour un point de coupe.
	 * <br/>Les arguments peuvent être protégés avec l'annotation {@link ProtectArgsFromLogging}.
	 *
	 * @param joinPoint Le point de coupe ({@link JoinPoint}).
	 * @return La chaîne de caractères détaillant les valeurs des arguments.
	 *
	 * @see #getMessage(JoinPoint).
	 */
	private String getArgs(final JoinPoint joinPoint) {
		// Récupération des arguments
		final Object[] objs = joinPoint.getArgs();

		// Note : les noms de paramètres ne sont pas accessibles dans l'aspect (sauf à être compilés par AspectJ)
		//final String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

		// Gestion des arguments protégés
		final List<Integer> args = new ArrayList<Integer>();
		final Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
		final ProtectArgsFromLogging protectArgsFromLogging = method.getAnnotation(ProtectArgsFromLogging.class);
		if (protectArgsFromLogging != null) {
			final int[] argIndexes = protectArgsFromLogging.argIndexes();
			for (final int element : argIndexes) {
				args.add(new Integer(element));
			}
		}

		// Construction de la chaîne de détails
		final StringBuilder sb = new StringBuilder();
		sb.append("(");
		for (int i = 0; i < objs.length; i++) {
			if (args.contains(new Integer(i))) {
				sb.append("[PROTECTED]");
			} else {
				// NB : attention aux particularités récursives des méthodes toString() (par exemple celles utilisant le ReflectionToStringBuilder)
				// ( --> peut générer des boucles infinies menant à des javaHeapSpace)
				sb.append(objs[i]);
			}

			if (i < objs.length - 1) sb.append(", ");
		}
		sb.append(")");

		return sb.toString();
	}
}
